package ui;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

import world.Ameisenhaufen;
import world.Futterhaufen;
import animal.Ameise;
import animal.FresstierInterface;
import animal.Kaefer;


/**
 * Dies ist ein Anzeigefeld, um Ameisenhaufen, Ameisen und beliebige weitere
 * Grafikobjekte vor einem Hintergrund darstellen zu koennen.
 * 
 * Das Koordinatensystem hat seinen Ursprung in der linken oberen Ecke. Die
 * X-Positionen der eingefuegten Objekte geben die Anzahl der Pixel nach rechts,
 * die Y-Position die Anzahl der Pixel nach unten an.
 * 
 * Die Position eines Objekts ist durch dessen Mitte definiert.
 * 
 * Die Drehung wird in Bogenmass gegen den Uhrzeigersinn angegeben.
 * 
 * @author Michael Schoell, Simone Huber, Shkelqim Turkaj
 * 
 */
public class Anzeigefeld extends JPanel {

	private Logger logger = Logger.getLogger(Anzeigefeld.class.getName());	
	private static final long serialVersionUID = 8651557749033794853L;
	// Die Hintergrundgrafik
	private String hintergrund = "grafiken/rasen1.jpg";
	// Der Cache fuer die Grafikdateien
	private HashMap<String, BufferedImage> images = new HashMap<String, BufferedImage>();
	// Speicher der zu zeichnenden Objekte
	private Vector<Object> originalobjekte = new Vector<Object>();
	private Map<Object,Integer> temporaryObjects = new ConcurrentHashMap<Object,Integer>();
	// Soll die Beschriftung der Grafikobjekte gezeichnet werden?
	private boolean beschriftungenZeigen = true;
	// Der String, der in der linken unteren Ecke gezeichnet werden soll
	private String anzeige;
	private int stepTime = 40;

	/**
	 * Generiert ein neues Anzeigefeld, das in ein JFrame einfuegbar ist. Das
	 * Feld wird automatisch alle 40 Millisekunden neu gezeichnet.
	 */
	public Anzeigefeld() {
		
			setDoubleBuffered(true);
			new Thread() {
				@Override
				public void run() {
					while (true) {
						//updateTempObjects();
						//neuZeichnen();
						try {
							Thread.sleep(stepTime);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}.start();
		}	
	
	private void updateTempObjects(){
		for(Map.Entry<Object,Integer> entry : temporaryObjects.entrySet()){	
			int time = entry.getValue();		
			if(time<0){			
				temporaryObjects.remove(entry);
			}else{		
				entry.setValue(time-stepTime);
			}
		}	
	}
	
	/**
	 * Zeigt das Anzeigefeld in einem Fenster auf dem Bildschirm.
	 */
	public void zeigeInFenster() {
		JFrame fenster = new JFrame();
		fenster.add(this);
		fenster.setPreferredSize(Toolkit.getDefaultToolkit().getScreenSize());
		fenster.setMinimumSize(Toolkit.getDefaultToolkit().getScreenSize());
		fenster.setVisible(true);
		fenster.setIconImage(Toolkit.getDefaultToolkit().createImage(
				"grafiken/ameise1.png"));
		fenster.setTitle("GPI1 - AmeisenSimulation von Shkelqim Turkaj & Daniel Schaubach");
		fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	}

	/**
	 * Zeichnet die gesamte Oberflaeche des Anzeigefeldes
	 */
	@Override
	public synchronized void paint(Graphics g) {
		Graphics2D g2d = (Graphics2D) g;
		if (getImage(hintergrund) != null) {
			g2d.drawImage(getImage(hintergrund), 0, 0, this.getWidth(), this
					.getHeight(), null);
		} else {
			g2d.clearRect(0, 0, this.getWidth(), this.getHeight());
		}

		paintTemporaryObjects(g2d);
		
		for (Object o : originalobjekte) {
			Grafikobjekt go = new Grafikobjekt();
			if (o instanceof Grafikobjekt) {
				go = (Grafikobjekt) o;
			} else {
				logger.severe("casting");
				cast(o, go);
			}

			
			zeichneGrafikobjekt(g2d, go);
		}

		if (beschriftungenZeigen) {
			if (anzeige != null) {
				g2d.drawString(anzeige, 15, 25);
			}
		}	
		
		g2d.drawString(
				"(c) copyright 2007 by sh und ms, Medizinische Informatik", 15,
				this.getHeight() - 15);
	}
	
	private void paintTemporaryObjects(Graphics2D g2d){		
		for(Object tempObj :  this.temporaryObjects.keySet()){			
			zeichneGrafikobjekt(g2d, (Grafikobjekt)tempObj);				
		}		
	}

	/**
	 * Zeichnet das Anzeigefeld neu.
	 */
	public void neuZeichnen() {
		repaint();
	}

	/**
	 * Setzt die Anzeige neu.
	 * @param anzeige Die Zeichenkette, die angezeigt werden soll
	 */
	public void setzeAnzeige(String anzeige) {
		this.anzeige = anzeige;
	}

	/**
	 * Zeichnet auf der uebergebenen Graphics2D das Grafikobjekt
	 * 
	 * @param g2d
	 * @param go
	 */
	private void zeichneGrafikobjekt(Graphics2D g2d, Grafikobjekt go) {
		BufferedImage image = getImage(go.getGrafik());

		int x = (int) go.getX();
		int y = (int) go.getY();
		
		boolean ausserhalb = false;

		if (x > this.getWidth()) {
			x = getWidth();
			ausserhalb = true;
		}
		if (y > this.getHeight()) {
			y = getHeight();
			ausserhalb = true;
		}
		if (x < 0) {
			x = 0;
			ausserhalb = true;
		}
		if (y < 0) {
			y = 0;
			ausserhalb = true;
		}

		if (image != null) {

			AffineTransform drehung = new AffineTransform();
			
			drehung.rotate(-go.getDrehung(),
					image.getWidth() / 2.0, image.getHeight() / 2.0);
			BufferedImageOp bio = new AffineTransformOp(drehung,
					AffineTransformOp.TYPE_BILINEAR);

			g2d.setColor(Color.black);
			g2d.setFont(new Font("Arial", Font.BOLD, 14));
			if (ausserhalb) {
				g2d.drawArc(x - 10, y - 10, 20, 20, 0, 360);
			}
			g2d.drawImage(image, bio, x - image.getWidth() / 2, y
					- image.getHeight() / 2);

			if (beschriftungenZeigen) {
				g2d.drawString(go.getBeschriftung(), x - 10, y - 30);
			}
		} else {
		}
	}

	/**
	 * Liefert zum uebergebenen Dateinamen das zugehoerige (gecachete)
	 * BufferedImage zurueck.
	 * 
	 * @param datei
	 *            Das Name der Grafikdatei
	 * @return null, wenn die Datei nicht gefunden wurde, sonst die Grafik als
	 *         BufferedImage
	 */
	private BufferedImage getImage(String datei) {
		BufferedImage image = null;
		if (images.containsKey(datei)) {
			image = images.get(datei);
		} else {
			try {
				File file = new File(datei);
				// ausgeben(datei);
				image = ImageIO.read(file);
			} catch (IOException e) {
				// ausgeben(e.getStackTrace().toString());
				ausgeben("Die Grafikdatei \"" + datei
						+ "\" wurde nicht gefunden.");
			}
			images.put(datei, image);
		}
		return image;
	}

	/*
	 * Methoden fuer das ganze Feld
	 */

	/**
	 * Gibt die Breite des Feldes zurueck
	 * 
	 * @return die Breite des Feldes
	 */
	public int getBreite() {
		return getWidth();
	}

	/**
	 * Gibt die Hoehe des Feldes zurueck
	 * 
	 * @return die Hoehe des Feldes
	 */
	public int getHoehe() {
		return getHeight();
	}

	/**
	 * Setzt den Dateinamen des Hintergrunds.
	 * 
	 * @param hintergrund
	 *            Dateiname des Hintergrunds
	 */
	public void setHintergrund(String hintergrund) {
		this.hintergrund = hintergrund;
	}

	/**
	 * Gibt an, ob die Beschriftungen der zu zeichnenden Objekte angezeigt
	 * werden sollen.
	 * 
	 * @return true, wenn sie gezeigt werden sollen.
	 */
	public boolean getBeschriftungenZeigen() {
		return beschriftungenZeigen;
	}

	/**
	 * Setzt, ob die Beschriftungen der zu zeichnenden Objekte angezeigt werden
	 * sollen.
	 * 
	 * @param beschriftungenZeigen
	 */
	public void setBeschriftungenZeigen(boolean beschriftungenZeigen) {
		this.beschriftungenZeigen = beschriftungenZeigen;
	}

	/*
	 * Methoden zum Manipulieren der Objekte *********************
	 */
	/**
	 * Setzt den Ameisenhaufen auf das Anzeigefeld.
	 * @param go der Ameisenhaufen, der neu gesetzt werden soll
	 */
	public synchronized void setzeAmeisenhaufen(Ameisenhaufen go) {
		setzeObjekt(go);
	}

	/**
	 * Entfernt den Ameisenhaufen vom Anzeigefeld.
	 * @param go der Ameisenhaufen, der entfernt werden soll
	 */
	public synchronized void entferneAmeisenhaufen(Ameisenhaufen go) {
		entferneObjekt(go, "Der Ameisenhaufen");
	}

	/**
	 * Setzt das Futter auf das Anzeigefeld.
	 * @param go das Futter, das neu gesetzt werden soll
	 */
	public synchronized void setzeFutter(Futterhaufen go) {
		setzeObjekt(go);
	}

	/**
	 * Entfernt das Futter vom Anzeigefeld.
	 * @param go das Futter, welches entfernt werden soll
	 */
	public synchronized void entferneFutter(Futterhaufen go) {
		entferneObjekt(go, "Das Futter");
	}

	/**
	 * Setzt das Fresstier auf das Anzeigefeld.
	 * @param go das Fresstier, das neu gesetzt werden soll
	 */
	public synchronized void setzeFresstier(FresstierInterface go) {
		setzeObjekt(go);
	}

	/**
	 * Entfernt das Fresstier vom Anzeigefeld.
	 * @param go das Fresstier, welches entfernt werden soll
	 */
	public synchronized void entferneFresstier(FresstierInterface go) {
		entferneObjekt(go, "Das Fresstier");
	}
	
	public synchronized void setzteTempGrafikobjekt(Object o, int time){
		if(!this.temporaryObjects.containsKey(o)){
			this.temporaryObjects.put(o,time);
		}
	}

	/**
	 * Setzt das Grafikobjekt auf das Anzeigefeld.
	 * @param go das Grafikobjekt, das neu gesetzt werden soll
	 */
	public synchronized void setzeGrafikobjekt(Grafikobjekt go) {
		setzeObjekt(go);
	}

	/**
	 * Entfernt das Graphikobjekt vom Anzeigefeld.
	 * @param go das Graphikobjekt, das entfernt werden soll
	 */
	public synchronized void entferneGrafikobjekt(Grafikobjekt go) {
		entferneObjekt(go, "Das Grafikobjekt");
	}

	/**
	 * Setzt die Ameise auf das Anzeigefeld.
	 * @param a die Ameise, die neu gesetzt werden soll
	 */
	public synchronized void setzeAmeise(Ameise a) {
		setzeObjekt(a);
	}

	/**
	 * Entfernt die Ameise vom Anzeigefeld.
	 * @param a die Ameise, die entfernt werden soll
	 */
	public synchronized void entferneAmeise(Ameise a) {
		entferneObjekt(a, "Die Ameise");
	}

	/**
	 * Entfernt die Ameise vom Anzeigefeld.
	 * @param a die Ameise, die entfernt werden soll
	 */
	public synchronized void entferneKaefer(Kaefer k) {
		entferneObjekt(k, "Der K�fer");
	}
	
	/*
	 * Die privaten Methoden, um die Objekte nun wirklich zu setzen
	 */

	/**
	 * Entfernt das uebergebene Objekt vom Anzeigefeld. Wenn es sich nicht auf
	 * dem Feld befand, wird eine Meldung ausgegeben.
	 * 
	 * @param o
	 *            das zu entfernenede Objekt
	 * @param name
	 *            der Name, der bei bei der Meldung als aussagekraeftigere
	 *            Identifikationsmoeglichkeit ausgegeben wird.
	 */
	private void entferneObjekt(Object o, String name) {
		if (o==null) {
			ausgeben(name + " ist null und kann daher nicht entfernt werden.");
		} else if (!originalobjekte.remove(o)) {
			ausgeben(name
					+ " war nicht auf dem Feld und konnte nicht entfernt werden.");
		}
	}

	/**
	 * Setzt das uebergebene Objekt. Dies muss jedoch die in Grafikobjekt
	 * angegebenen Eigenschaften besitzen.
	 * 
	 * @param o
	 *            ein Objekt mit den selben privaten Feldern wie Grafikobjekt
	 */
	private void setzeObjekt(Object o) {
		originalobjekte.add(o);
	}

	/*
	 * Statische Methoden **********************************************+
	 */

	/**
	 * Wandelt das uebergebene Objekt in ein Grafikobjekt mit gleichen
	 * Eigenschaften um. Wenn moeglich wird dies durch einen Cast gemacht, wenn
	 * nicht moeglich, ueber Reflections. Dafuer muessen alle Felder und Methoden
	 * in der Klasse selbst oder *einer* der Ueberklassen definiert sein.
	 * 
	 * @param <T> Typ des Quellobjekts
	 * @param <S> Typ des Zielobjekts
	 * @param quellobjekt Objekt, welches in ein Grafikobjekt umgewandelt werden soll
	 * @param zielobjekt Zielobjekt, in welches das Grafikobjekt umgewandelt werden soll
	 * @return
	 */
	private static <T, S> void cast(T quellobjekt, S zielobjekt) {
		Class<?> zielKlasse = zielobjekt.getClass();
		Class<?> quellKlasse = quellobjekt.getClass();

		boolean erfolg = false;

		while (!quellKlasse.equals(Object.class) && !erfolg) {
			// alle privaten Felder holen
			try {
				for (Field zielFeld : zielKlasse.getDeclaredFields()) {

					// Field quellFeld = quellKlasse.getDeclaredField(zielFeld
					// .getName());
					String zielfeldName = zielFeld.getName();

					Method quellMethode = quellKlasse.getDeclaredMethod("get"
							+ zielfeldName.substring(0, 1).toUpperCase()
							+ zielfeldName.substring(1), (Class[]) null);

					zielFeld.setAccessible(true);
					quellMethode.setAccessible(true);

					if ((zielFeld.getType().equals(boolean.class))) {
						zielFeld.setBoolean(zielobjekt, (Boolean) quellMethode
								.invoke(quellobjekt, (Object[]) null));
					} else if ((zielFeld.getType().equals(byte.class))) {
						zielFeld.setByte(zielobjekt, (Byte) quellMethode
								.invoke(quellobjekt, (Object[]) null));
					} else if ((zielFeld.getType().equals(char.class))) {
						zielFeld.setChar(zielobjekt, (Character) quellMethode
								.invoke(quellobjekt, (Object[]) null));
					} else if ((zielFeld.getType().equals(double.class))) {
						zielFeld.setDouble(zielobjekt, (Double) quellMethode
								.invoke(quellobjekt, (Object[]) null));
					} else if ((zielFeld.getType().equals(float.class))) {
						zielFeld.setFloat(zielobjekt, (Float) quellMethode
								.invoke(quellobjekt, (Object[]) null));
					} else if ((zielFeld.getType().equals(int.class))) {
						zielFeld.setInt(zielobjekt, (Integer) quellMethode
								.invoke(quellobjekt, (Object[]) null));
					} else if ((zielFeld.getType().equals(long.class))) {
						zielFeld.setLong(zielobjekt, (Long) quellMethode
								.invoke(quellobjekt, (Object[]) null));
					} else if ((zielFeld.getType().equals(short.class))) {
						zielFeld.setShort(zielobjekt, (Short) quellMethode
								.invoke(quellobjekt, (Object[]) null));
					} else {
						zielFeld.set(zielobjekt, quellMethode.invoke(
								quellobjekt, (Object[]) null));
					}
				}
				erfolg = true;
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (InvocationTargetException ite) {
				ite.printStackTrace();
			} catch (SecurityException e) {
				e.printStackTrace();
			} catch (NoSuchMethodException e) {
				// versucht es noch mal mit der Superklasse (fuer geerbte
				// Methoden und Felder)
				quellKlasse = quellKlasse.getSuperclass();
				erfolg = false;
			}
		}
		if (!erfolg) {
			ausgeben("Die darzustellenden Objekte muessen die gleichen privaten Felder mit zugehoerigen get- und set-Methoden besitzen wie die Klasse Grafikobjekt!\n"
					+ "Falls dies der Fall ist, bitte diesen Fehler an Simone oder Michael melden!");
		}
	}

	/**
	 * Gibt eine Meldung aus.
	 * 
	 * @param meldung
	 *            Die Meldung
	 */
	private static void ausgeben(String meldung) {
		// TODO
		System.out.println(meldung);
	}

	/*
	 * Testmethoden **********************************************+
	 */

	// @Test
	// public void testCast() {
	// Ameise ameise = new Ameise();
	// ameise.setDrehung(1);
	// ameise.setGrafik("2");
	// ameise.setX(3);
	// ameise.setY(4);
	// Grafikobjekt go = new Grafikobjekt();
	// cast(ameise, go);
	// assertTrue(go.getDrehung() == ameise.getDrehung());
	// assertTrue(go.getGrafik() == ameise.getGrafik());
	// assertTrue(go.getX() == 3);
	// assertTrue(go.getY() == 4);
	// }
}
